<?php

class SerializationException extends Exception {
    
}


/**
 * <p>Serializes and de-serializes objects to/from a string.</p>
 * 
 * <p>The default implementations are {@link EntitySerializerXML} and
 * {@link EntitySerializerJSON} that decodes and encodes objects to and from
 * XML and JSon respectively.</p>
 *
 * <p>This class can be extended to provide any kind of string serialization
 * format e.g. YAML or OWL </p>
 *
 * @author JGuevara
 * @see MimeSerializerFactory
 */
abstract class EntitySerializer {

    private $propertiesArePublic = false;
    private $getPrefix = "get";
    private $setPrefix = "set";
    private $targetClass = "stdClass";
    private $mapping = array();

    public function __construct($targetClass = "stdClass",
            array $mapping = array()) {
        $this->targetClass = $targetClass;
        $this->mapping = $mapping;
    }

    public function serialize($object) {
        $array = $this->getProperties($object);
        return $this->doSerialize($array, $this->targetClass);
    }

    public function deSerialize($string) {
        $Class = $this->targetClass;
        $newObject = new $Class();
        $this->deSerializeTo($string, $newObject);
        return $newObject;
    }

    public function deSerializeTo($string, &$targetObject) {
        $array = $this->convertStringToArray($string);
        if (!is_array($array)) {
            throw new DeserializationException($string, $array);
        }
        $this->doDeserialize($array, $targetObject, $this->mapping);
        unset($array);
    }

    public abstract function serializeArray(array $array, $wrapper = "");

    /**
     * <p>De-Serializes a string, but the first level of elements is treated as
     * an array of objects.
     * </p>
     * <p>For example, if we have this JSON string: </p>
     * <code>{
     *  "Object1":{"attr":"value1"},
     *  "Object2":{"attr":"value2"},
     *  "Object3":{"attr":"value3"},
     *  "ObjectN":{"attr":"valueN"}
     * }</code>
     * <p>Then the result might be something like:</p>
     * <code>
     * array(
     *  "Object1"=>new Object("value1"),
     *  "Object2"=>new Object("value2"),
     *  "Object3"=>new Object("value3"),
     *  "ObjectN"=>new Object("valueN"),
     * );
     * </code>
     * @param string $string The string to de-serialize from.
     * @return array An array of instances of the target class.
     */
    public function deserializeArray($string) {
        $array = $this->convertStringToArray($string);
        $return = array();
        if ($array != null) {
            foreach ($array as $key => $object) {
                if (!is_array($object)) {
                    throw new DeserializationException($string, $array);
                }
                $Class = $this->targetClass;
                $entity = new $Class();
                $this->doDeserialize($object, $entity, $this->mapping);
                $return[$key] = $entity;
            }
        }
        unset($array);
        return $return;
    }

    /**
     * <p>Asigns the values of the array to the corresponding properties of the
     * object.</p>
     *
     * <p><strong>Note:</strong> The array values given to this function are set
     * to <code>null</code> during the object construction, the array should be
     * eventually deleted.
     * 
     * @param array $array The properties to copy from.
     * @param Class $targetObject The object in wich the values will be put.
     * @param array $mapping
     */
    private function doDeserialize(array &$array, &$targetObject,
            array &$mapping = array()) {

        foreach ($array as $propertyName => &$propertyValue) {
            if (is_scalar($propertyValue)) {
                $this->invokeSet($propertyName, $propertyValue, $targetObject);
            } else {
                //Determinar si existe una clase para este objeto compuesto.
                if (array_key_exists($propertyName, $mapping)) {

                    $InnerClassName = $mapping[$propertyName];
                    $innerMapping = array();
                    //Determine if there is any internal mapping for the inner object.
                    if (array_key_exists($propertyName . ".mapping", $mapping)) {
                        $innerMapping = $mapping[$propertyName . ".mapping"];
                    }

                    if ($array[$propertyName] != null) {
                        $newobject = new $InnerClassName();
                        $this->doDeserialize($array[$propertyName], $newobject, $innerMapping);
                        $this->invokeSet($propertyName, $newobject, $targetObject);
                    }
                } else if (array_key_exists("$propertyName.array", $mapping) && is_array($propertyValue)) {
                    $InnerClassName = $mapping["$propertyName.array"];
                    $innerMapping = array();
                    //Determine if there is any internal mapping for the inner object.
                    if (array_key_exists("$propertyName.array.mapping", $mapping)) {
                        $innerMapping = $mapping["$propertyName.array.mapping"];
                    }

                    $list = array();
                    foreach ($propertyValue as &$object) {
                        if ($object != null) {
                            $newobject = new $InnerClassName();
                            $this->doDeserialize($object, $newobject, $innerMapping);
                            array_push($list, $newobject);
//                            array_push($list, $object);
                        }
                    }

                    $this->invokeSet($propertyName, $list, $targetObject);
                }
            }

            $array[$propertyName] = null;
            unset($array[$propertyName]);
        }
    }

    /**
     * Implementor must interpret the given string and convert it into
     * an asociative array.
     *
     * @param string $string The string that represents the object, normally XML
     * or JSon.
     *
     * @return array An associative array wich will be then converted into the
     * target object.
     */
    protected abstract function convertStringToArray($string);

    /**
     * Implementor must convert the given associative array into the corresponding
     * string represetation.
     *
     * @param array $array The object representation.
     * @param string $rootName [Optional] The root element name.
     *
     * @return string The string representation of the object, normally
     * XML or JSon.
     */
    protected abstract function doSerialize(array $array, $rootName = "");

    /**
     * Wraps a given string.
     * 
     * @param string $string The string to be wrapped.
     * @param string $paramName The tag to envelop in the given string.
     * @param string $methodName An optional root. Some implementations might ignores this.
     * @return string A new wrapped string
     */
    public abstract function wrap($string, $paramName, $methodName="");

    /**
     * Wraps an array of strings.
     *
     * @param array $array The key-valued array of strings where each key is a param name.
     * @param string $methodName An optional root. This might be ignoded by some implementation.
     */
    public abstract function wrapArray($array = array(), $methodName="");

    public function getPropertiesArePublic() {
        return $this->propertiesArePublic;
    }

    public function setPropertiesArePublic($propertiesArePublic) {
        $this->propertiesArePublic = $propertiesArePublic;
    }

    public function getGetPrefix() {
        return $this->getPrefix;
    }

    public function setGetPrefix($getSuffix) {
        $this->getPrefix = $getSuffix;
    }

    public function getSetPrefix() {
        return $this->setPrefix;
    }

    public function setSetPrefix($setSuffix) {
        $this->setPrefix = $setSuffix;
    }

    public function getTargetClass() {
        return $this->targetClass;
    }

    public function setTargetClass($targetClass) {
        $this->targetClass = $targetClass;
    }

    public function getMapping() {
        return $this->mapping;
    }

    public function setMapping($mapping) {
        $this->mapping = $mapping;
    }

    /**
     * Traduces an object into an associative array of the object
     * 
     * @param object $targetObject
     * @return array 
     */
    private function getProperties($targetObject) {

        $result = array();

        if (is_array($targetObject)) {
            foreach ($targetObject as $object) {
                $result[] = $this->getProperties($object);
            }
            return $result;
        }
        

        if (!$this->propertiesArePublic) {
            $reflectedObject = new ReflectionClass($targetObject);
            foreach ($reflectedObject->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {

                if (stripos($method->name, $this->getPrefix) === 0) {
                    $propertyValue = $method->invoke($targetObject);
                    if (!is_object($propertyValue) && !is_array($propertyValue)) {
                        $result[str_replace($this->getPrefix, "", $method->name)] = $propertyValue;
                    } else {
                        $result[str_replace($this->getPrefix, "", $method->name)] = $this->getProperties($propertyValue);
                    }
                }
            }
        } else {
            $result = get_object_vars($targetObject);
        }

        return $result;
    }

//    private function getPropertiesFromArray($array){
//        $result = array();
//        foreach ($array as $key => $value) {
//            $result[$key] = $this->getProperties($value);
//        }
//
//        return $result;
//    }

    private function invokeSet($propertyName, &$propertyValue, &$targetObject) {
        if (!$this->propertiesArePublic) {
            $method = $this->setPrefix . $propertyName;
            if (method_exists($targetObject, $method)) {
                $targetObject->$method($propertyValue);
            }
        } else {
            $targetObject->$propertyName = $propertyValue;
        }
    }

}

class EntitySerializerXML extends EntitySerializer {

    /**
     * Wraps a given string into tags which name is the $paramName. Optionally
     * you can give a root tag.
     *
     * @param string $string The string to be wrapped.
     * @param string $paramName The tag to envelop in the given string. For this implementation the result will be:
     * <code>&lt;$paramName&gt;$string&lt;/$paramName&gt;</code>
     * @param string $methodName An optional root. For this implementation result would be:
     * <code>
     * &lt;$methodName&gt;
     *  &lt;$paramName&gt;$string&lt;/$paramName&gt;
     * &lt;/$methodName&gt;
     * </code>
     * @return string A new wrapped string
     */
    public function wrap($string, $paramName, $methodName="") {
        $wraped = "<$paramName>$string</$paramName>";

        if ($methodName !== "") {
            $wraped = $this->wrap($wraped, $methodName);
        }

        return $wraped;
    }

    /**
     * Wraps an array of strings.
     *
     * @param array $array The key-valued array of strings where each key is a param name.
     * @param string $methodName An optional root. This might be ignoded by some implementation.
     */
    public function wrapArray($array = array(), $methodName="") {
        $wraped = "";
        foreach ($array as $tag => $value) {
            $wraped .= $this->wrap($value, $tag);
        }

        if ($methodName !== "") {
            $wraped = $this->wrap($wraped, $methodName);
        }

        return $wraped;
    }

    protected function convertStringToArray($string) {
        return get_object_vars(simplexml_load_string($string));
    }

    protected function doSerialize(array $array, $rootName = "") {
        $docXml = new DOMDocument('1.0', 'utf-8');
        $xml = $docXml->createElement($rootName);
        $xml = $docXml->appendChild($xml);
        $this->recursivePropertiesToXml($docXml, $xml, $array);
        return $docXml->saveXML();
    }

    private function recursivePropertiesToXml($document, $xmlNode, $values) {
        foreach ($values as $k => $value) {
            //Puts the get method... without the 'get'!
            $domProperty = $document->createElement((is_numeric($k) ? "_" : "") . $k);
            $domProperty = $xmlNode->appendChild($domProperty);

            if (is_scalar($value)) {
                $valClef = $document->createTextNode($value);
                $valClef = $domProperty->appendChild($valClef);
            } else {
                if (is_array($value) || is_object($value)) {
                    $this->recursivePropertiesToXml($document, $domProperty, $value);
                }
            }
        }
    }

    public function serializeArray(array $array, $wrapper = "") {
        $result = "";
        foreach ($array as $object) {
            $result .= parent::serialize($object);
        }

        $result = "<$wrapper>$result</$wrapper>";
    }

}

class EntitySerializerJSON extends EntitySerializer {

    protected function convertStringToArray($string) {
        return json_decode($string, true);
    }

    protected function doSerialize(array $array, $rootName = "") {
        return json_encode($array);
    }

    public function wrap($string, $paramName, $methodName = "") {
        return '{"' . $paramName . '":' . $this->processValue($string) . '}';
    }

    public function wrapArray($array = array(), $methodName = "") {
        $wrapped = "{";
        $count = 0;
        foreach ($array as $key => $value) {
            if ($count != 0) {
                $wrapped .= ",";
            }
            $wrapped .= '"' . $key . '":' . $this->processValue($value);
            $count++;
        }
        $wrapped .= "}";
        return $wrapped;
    }

    private function processValue($value) {
        if (is_numeric($value) || preg_match("/{*}/i", $value)) {
            return $value;
        } else {
            return "\"$value\"";
        }
    }

    public function serializeArray(array $array, $wrapper = "") {
        $result = "";
        $counter = 0;
        foreach ($array as $object) {
            if ($counter !== 0) {
                $result .= ", ";
            }
            $result .= parent::serialize($object);

            $counter++;
        }

        $result = "[$result]";

        return $result;
    }

}

class MimeSerializerFactory {

    /**
     * <p>Instantiates an EntitySerializer according to the mime type.</p>
     *
     * <p><strong>Node: </strong> The first part of the mime (the one before the '/'),
     * is <em>ignored</em>, so, the mime types: application/xml and text/xml will
     * cause the instantiation of the same EntitySerializer type.
     * </p>
     *
     * <h3>Supported mime/types</h3>
     *
     * <dl>
     *  <dt>(*)/xml</dt>
     *  <dd>
     *      Can be application/xml, text/xml... etc. Creates an EntitySerializerXML
     *  </dd>
     *  <dt>(*)/json</dt>
     *  <dd>
     *      Can be application/json, text/json... etc. Creates an EntitySerializerJSON
     *  </dd>
     * </dl>
     *
     * @param string $mimeType A supported mime type name.
     * @param string $targetClass The class name to be serialized to/from. stdClass by default.
     * @param array $mapping The mapping for the target class if it has composite objects.
     * @param boolean $propertiesArePublic [Optional] If rather or not the target class properties are public, <code>false</code> by default
     * @param string $getSuffix [Optional] The property get suffix, "get" by default.
     * @param string $setSuffix [Optional] The property set suffix, "set" by default.
     *
     * @return EntitySerializer depending on the mime type where XXX is the mime kind JSON or XML or
     * any other if you add suport for it.
     *
     * @see EntitySerializer
     * @see EntitySerializerXML
     * @see EntitySerializerJSON
     */
    public static function getByMimeType($mimeType, $targetClass = "stdClass",
            array $mapping = array()) {
        //Obtiene el sufijo de la implementacion.
        $slashPos = strrpos($mimeType, "/");
        $suffix = strtoupper(substr($mimeType, $slashPos + 1));
        //Instancia la implementacion
        $reflected = new ReflectionClass('EntitySerializer' . $suffix);
        $serializer = $reflected->newInstance($targetClass, $mapping);
        return $serializer;
    }

}

?>
